<feature>[longjob]: Introduce suspend long job message#3241
<feature>[longjob]: Introduce suspend long job message#3241ZStack-Robot wants to merge 1 commit into5.5.6from
Conversation
Walkthrough本变更为长期任务引入“暂停(suspend)”功能:新增 API 消息/事件、LongJob 接口与工厂的扩展、工厂实现的支持检测、管理器的暂停处理流程、可恢复错误判定、测试封装与中文文档,以及少量常量与钩子更新。 Changes
Sequence Diagram(s)sequenceDiagram
participant Client as "Client"
participant API as "API Handler"
participant Manager as "LongJobManager"
participant Factory as "LongJobFactory"
participant Job as "LongJob Impl"
participant DB as "Database"
Client->>API: APISuspendLongJobMsg(uuid)
API->>Manager: handle(APISuspendLongJobMsg)
Manager->>Manager: enqueue ChainTask("suspend-longjob-"+uuid)
Manager->>DB: query LongJobVO(uuid)
alt already Suspended
Manager-->>API: publish APISuspendLongJobEvent(success)
else state != Running
Manager-->>API: publish APISuspendLongJobEvent(error)
else
Manager->>Factory: supportSuspend(jobName)?
alt not supported
Manager-->>API: publish APISuspendLongJobEvent(error)
else supported
Manager->>Job: suspend(LongJobVO, completion)
alt suspend success
Job->>DB: update state -> Suspended
Manager-->>API: publish APISuspendLongJobEvent(success)
else suspend failure
Manager-->>API: publish APISuspendLongJobEvent(error)
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.40.5)utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.javaComment |
6c098e1 to
af47416
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java`:
- Around line 484-501: The suspend path lacks the intermediate "Suspending"
state used by cancelLongJob, which can allow concurrent suspend calls to race;
before calling job.suspend(...) in suspendLongJob, set the intermediate state
(e.g., changeState(uuid, LongJobStateEvent.suspending) or the appropriate enum)
while holding the same synchronization (synchronized(jobUuids.intern(uuid))) as
cancelLongJob, then call job.suspend; on success move to Suspended (as now) and
on failure revert or record the proper state and propagate the error, mirroring
the cancelLongJob pattern (use the same locking, changeState calls, and error
handling around job.suspend to ensure consistency).
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (1)
463-472: 可考虑优化:减少重复数据库查询在第 464-466 行查询了
state和jobName,但在第 469 行又执行了一次findByUuid获取完整的LongJobVO。可以考虑在需要返回 VO 时直接查询完整对象,或者延迟到真正需要时再查询。♻️ 建议的优化
private void suspendLongJob(String uuid, ReturnValueCompletion<LongJobVO> completion) { Tuple t = Q.New(LongJobVO.class).eq(LongJobVO_.uuid, uuid).select(LongJobVO_.state, LongJobVO_.jobName).findTuple(); LongJobState currentState = t.get(0, LongJobState.class); String jobName = t.get(1, String.class); if (currentState == LongJobState.Suspended) { - LongJobVO vo = dbf.findByUuid(uuid, LongJobVO.class); - completion.success(vo); + completion.success(dbf.findByUuid(uuid, LongJobVO.class)); return; }
| LongJobVO vo = dbf.findByUuid(uuid, LongJobVO.class); | ||
| LongJob job = longJobFactory.getLongJob(vo.getJobName()); | ||
|
|
||
| job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) { | ||
| @Override | ||
| public void success(Boolean suspended) { | ||
| LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); | ||
| logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); | ||
| completion.success(updatedVo); | ||
| } | ||
|
|
||
| @Override | ||
| public void fail(ErrorCode errorCode) { | ||
| logger.error(String.format("failed to suspend longjob [uuid:%s, name:%s]: %s", vo.getUuid(), vo.getName(), errorCode)); | ||
| completion.fail(errorCode); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 查找 LongJobState 和 LongJobStateEvent 枚举定义
find . -name "*.java" -type f | xargs grep -l "enum LongJobState\|enum LongJobStateEvent" | head -5Repository: MatheMatrix/zstack
Length of output: 253
🏁 Script executed:
# 在 LongJobManagerImpl.java 中查找 cancelLongJob 和其他状态转换方法
rg -n "cancelLongJob|suspendLongJob|resumeLongJob|changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 5 -B 1Repository: MatheMatrix/zstack
Length of output: 6832
🏁 Script executed:
# 查找 LongJobState 和 LongJobStateEvent 的定义
rg -n "enum LongJobState|enum LongJobStateEvent" --type=java -A 25Repository: MatheMatrix/zstack
Length of output: 4185
🏁 Script executed:
# 查看 Running 状态的所有可能转移
rg -n "Running\.transactions" header/src/main/java/org/zstack/header/longjob/LongJobState.java -A 10Repository: MatheMatrix/zstack
Length of output: 750
🏁 Script executed:
# 查看 changeState 方法的实现
rg -n "private.*changeState|public.*changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 15Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 用不同方式查找 changeState 方法
rg -n "changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | head -20Repository: MatheMatrix/zstack
Length of output: 824
🏁 Script executed:
# 查找 changeState 的完整实现
rg -n "def changeState|LongJobVO changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 20Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 使用 ast-grep 查找 changeState 方法
ast-grep --pattern 'LongJobVO changeState($_, $_) { $$$ }'Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 查找 changeState 方法定义,搜索返回类型
rg -n "changeState\(" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -B 2 | grep -E "private|public|protected" -A 2Repository: MatheMatrix/zstack
Length of output: 1026
🏁 Script executed:
# 在整个文件中搜索方法签名
cat longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "changeState" -B 5 | head -50Repository: MatheMatrix/zstack
Length of output: 2176
🏁 Script executed:
# 直接查看文件的特定行范围,寻找 changeState 方法定义
wc -l longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.javaRepository: MatheMatrix/zstack
Length of output: 131
🏁 Script executed:
# 查找 changeState 的完整定义(在文件后部分)
tail -400 longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | head -200 | grep -n "changeState" -B 5 -A 15Repository: MatheMatrix/zstack
Length of output: 4088
🏁 Script executed:
# 搜索所有的方法定义,找到 changeState
rg -n "^\s*(private|public|protected|static).*changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.javaRepository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 用 sed 查找 changeState 方法定义
sed -n '900,1005p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "changeState" -B 5 -A 20Repository: MatheMatrix/zstack
Length of output: 1126
🏁 Script executed:
# 使用 sed 直接查看文件特定行数范围
sed -n '750,850p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.javaRepository: MatheMatrix/zstack
Length of output: 3523
🏁 Script executed:
# 搜索 LongJobVO 返回类型的方法定义
rg -n "LongJobVO.*changeState|changeState.*\{" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -B 1 -A 20 | head -80Repository: MatheMatrix/zstack
Length of output: 4100
🏁 Script executed:
# 搜索 private/public 修饰的 changeState 方法
sed -n '800,900p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "changeState\|private\|public"Repository: MatheMatrix/zstack
Length of output: 513
🏁 Script executed:
# 用 awk 找到 changeState 方法的确切位置
awk '/private.*changeState|public.*changeState/{print NR":"$0; for(i=1;i<=30;i++){getline; print NR":"$0}}' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | head -60Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 从后往前搜索 changeState 的方法定义
rg -n "^\s*private\s+LongJobVO\s+changeState|^\s*public\s+LongJobVO\s+changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 30Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 直接查看整个文件,搜索所有的 LongJobVO changeState 方法
grep -n "LongJobVO changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -A 25Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 搜索包含 changeState 的所有行
rg "changeState" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java -nRepository: MatheMatrix/zstack
Length of output: 824
🏁 Script executed:
# 尝试搜索方法定义的其他模式
rg -n "changeState.*\(" longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | tail -20Repository: MatheMatrix/zstack
Length of output: 824
🏁 Script executed:
# 查看文件末尾部分,changeState 可能定义在那里
tail -200 longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.javaRepository: MatheMatrix/zstack
Length of output: 7196
🏁 Script executed:
# 查找 LongJobManagerImpl 的类定义和继承
head -50 longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | grep -n "class\|extends\|implements"Repository: MatheMatrix/zstack
Length of output: 44
🏁 Script executed:
# 搜索所有文件中的 changeState 方法定义
rg -n "LongJobVO.*changeState\s*\(" --type=java -B 2 -A 20 | head -100Repository: MatheMatrix/zstack
Length of output: 10796
可考虑添加中间状态以保持一致性,但当前设计可能依赖幂等性
对比 cancelLongJob 的实现,取消操作在调用 job.cancel() 之前会先设置 Canceling 中间状态(第 319 行),以防止并发请求干扰。而 suspendLongJob 在调用 job.suspend() 前没有设置中间状态,而是在成功后才通过 changeState() 改为 Suspended 状态(第 490 行)。
这意味着两个并发的 suspend 请求都可能通过 Running 状态检查(第 465 行)并调用 job.suspend()。不过值得注意的是:
changeState()方法使用了synchronized (jobUuids.intern(uuid))进行同步化处理- 状态机允许
Suspended → suspend → Suspended的幂等转换
这表明当前设计可能依赖 suspend 操作的幂等性。建议明确文档说明 suspend 操作是否设计为幂等的,或考虑添加 Suspending 中间状态以与 cancel 的设计模式保持一致。
🤖 Prompt for AI Agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java` around
lines 484 - 501, The suspend path lacks the intermediate "Suspending" state used
by cancelLongJob, which can allow concurrent suspend calls to race; before
calling job.suspend(...) in suspendLongJob, set the intermediate state (e.g.,
changeState(uuid, LongJobStateEvent.suspending) or the appropriate enum) while
holding the same synchronization (synchronized(jobUuids.intern(uuid))) as
cancelLongJob, then call job.suspend; on success move to Suspended (as now) and
on failure revert or record the proper state and propagate the error, mirroring
the cancelLongJob pattern (use the same locking, changeState calls, and error
handling around job.suspend to ensure consistency).
a5268ac to
0ac0d2c
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobFactory.java (1)
10-15: 新增接口方法建议补充 Javadoc
Line 14 新增接口方法但未补充 Javadoc,建议按规范补齐以便生成文档与保持一致性。📝 建议修改
boolean supportCancel(String jobName); + /** + * `@param` jobName long job name + * `@return` true if suspend is supported for the job type + */ boolean supportSuspend(String jobName);As per coding guidelines, ...
♻️ Duplicate comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (1)
484-501: 与cancelLongJob相比缺少中间状态的问题已在之前的评审中提出。当前实现在调用
job.suspend()之前没有设置中间状态(如Suspending),而cancelLongJob会在第 319 行先设置Canceling状态。虽然changeState内部使用了synchronized(jobUuids.intern(uuid))进行同步,但这可能依赖于 suspend 操作的幂等性设计。建议确认 suspend 操作是否设计为幂等的,或考虑添加
Suspending中间状态以与 cancel 模式保持一致。
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)
172-189: 建议将 "longJobRecoverable" 提取为常量
Line 185 使用字符串字面量作为跨模块约定的 key,存在拼写不一致风险。建议提为常量(必要时可公开)以减少魔法值。♻️ 建议修改
public class LongJobUtils { private static final CLogger logger = Utils.getLogger(LongJobUtils.class); + public static final String LONG_JOB_RECOVERABLE_OPAQUE_KEY = "longJobRecoverable"; @@ - Object recoverable = errorCode.getFromOpaque("longJobRecoverable"); + Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_OPAQUE_KEY);As per coding guidelines, ...
0ac0d2c to
8d07640
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)
160-169: 检查可恢复标记在聚合错误的递归传播路径
ErrorCodeList extends ErrorCode并包含List<ErrorCode> causes字段,若业务侧将longJobRecoverable标记设置在causes列表中的错误对象的opaque字段上,当前的isRecoverableError方法(仅检查顶层getFromOpaque)无法检测到该标记,导致判定为 fail 而非 suspend。建议补充递归遍历 causes 的逻辑,或在接口文档中明确要求该标记必须设置在顶层错误的 opaque 字段上。
🤖 Fix all issues with AI agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java`:
- Around line 487-493: The success handler for job.suspend currently ignores the
suspended Boolean and always transitions the job to Suspended; modify the
anonymous ReturnValueCompletion<Boolean> in the job.suspend(...) call so it
checks the suspended value and only calls changeState(uuid,
LongJobStateEvent.suspend) and completion.success(...) when suspended == true,
and otherwise call completion.fail(...) or completion.success(vo) (to keep
original state) per the method contract; update the success(Boolean suspended)
method in that block and ensure logging reflects whether the suspend actually
succeeded.
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)
182-186: 将 opaque key 抽成常量,避免魔法字符串
建议把"longJobRecoverable"提取为私有常量,降低拼写风险和复用成本。♻️ 建议修改
public class LongJobUtils { private static final CLogger logger = Utils.getLogger(LongJobUtils.class); + private static final String LONG_JOB_RECOVERABLE_OPAQUE_KEY = "longJobRecoverable"; @@ - Object recoverable = errorCode.getFromOpaque("longJobRecoverable"); + Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_OPAQUE_KEY);
| job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) { | ||
| @Override | ||
| public void success(Boolean suspended) { | ||
| LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); | ||
| logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); | ||
| completion.success(updatedVo); | ||
| } |
There was a problem hiding this comment.
必须处理 suspended 返回值,否则可能错误地标记为 Suspended。
当前无论 job.suspend(...) 返回 true/false 都会切到 Suspended。若实现返回 false 表示未真正暂停,这里会造成状态错误。
建议仅在 suspended == true 时变更状态,或按契约将 false 视为失败/保持原状态。
🔧 建议修正示例
job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) {
`@Override`
public void success(Boolean suspended) {
- LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
- logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
- completion.success(updatedVo);
+ if (Boolean.TRUE.equals(suspended)) {
+ LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend);
+ logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName()));
+ completion.success(updatedVo);
+ } else {
+ logger.info(String.format("longjob [uuid:%s, name:%s] suspend returned false; keep state unchanged", vo.getUuid(), vo.getName()));
+ completion.success(dbf.findByUuid(uuid, LongJobVO.class));
+ }
}🤖 Prompt for AI Agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java` around
lines 487 - 493, The success handler for job.suspend currently ignores the
suspended Boolean and always transitions the job to Suspended; modify the
anonymous ReturnValueCompletion<Boolean> in the job.suspend(...) call so it
checks the suspended value and only calls changeState(uuid,
LongJobStateEvent.suspend) and completion.success(...) when suspended == true,
and otherwise call completion.fail(...) or completion.success(vo) (to keep
original state) per the method contract; update the success(Boolean suspended)
method in that block and ensure logging reflects whether the suspend actually
succeeded.
50a007a to
06d193f
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy`:
- Around line 10-23: The documentation contains missing placeholder descriptions
for the APISuspendLongJobEvent fields; update the ref block for "inventory"
(path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory", clz
LongJobInventory) to include a concise Chinese description of what the
LongJobInventory represents (e.g., long job metadata being suspended), and
update the field block "success" to include a Chinese description indicating
whether the suspend operation succeeded (boolean true/false); ensure both desc
strings are meaningful, concise, and match existing doc style.
- Line 8: Replace the placeholder title string in
APISuspendLongJobEventDoc_zh_cn.groovy: change the line title "在这里输入结构的名称" to a
proper Chinese title such as "暂停长任务事件" (or the English identifier
"APISuspendLongJobEvent") so the document header accurately reflects the
structure; update only the title(...) call in that file to the chosen
descriptive text.
♻️ Duplicate comments (2)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (2)
487-493: 必须处理suspended返回值当前实现忽略了
job.suspend()的返回值suspended,无论其为true还是false都会将状态变更为Suspended。如果实现方返回false表示暂停未真正执行,这将导致状态与实际不一致。建议仅在
suspended == true时变更状态。建议修复
job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) { `@Override` public void success(Boolean suspended) { - LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); - logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); - completion.success(updatedVo); + if (Boolean.TRUE.equals(suspended)) { + LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); + logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); + completion.success(updatedVo); + } else { + logger.info(String.format("longjob [uuid:%s, name:%s] suspend returned false, state unchanged", vo.getUuid(), vo.getName())); + completion.success(dbf.findByUuid(uuid, LongJobVO.class)); + } }
484-486: 建议:考虑添加中间状态以保持一致性对比
cancelLongJob在调用job.cancel()前会先设置Canceling中间状态(Line 319),而suspendLongJob在调用job.suspend()前没有设置中间状态。这可能导致并发请求同时通过Running状态检查。如果 suspend 操作设计为幂等的,当前实现可以接受;否则建议添加
Suspending中间状态。
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobUtils.java (1)
182-190: 建议提取魔法值为常量并简化布尔判断逻辑。根据编码规范,应避免使用魔法值。字符串
"longJobRecoverable"建议定义为常量。同时,布尔判断可以使用Boolean.TRUE.equals()简化,更加简洁且能安全处理 null 和非 Boolean 类型。♻️ 建议的重构
+ public static final String LONG_JOB_RECOVERABLE_KEY = "longJobRecoverable"; + /** * Check if an error is marked as recoverable in its opaque field. * Any business module can mark an error as recoverable by setting * "longJobRecoverable" to true in the error code's opaque field. * This allows the long job framework to automatically suspend instead of fail * for recoverable errors, enabling automatic retry after service restart. * * `@param` errorCode the error code to check * `@return` true if the error is marked as recoverable, false otherwise */ private static boolean isRecoverableError(ErrorCode errorCode) { - // Check if error code has recoverable flag in opaque - // Any business module can set this flag to indicate the error is recoverable - Object recoverable = errorCode.getFromOpaque("longJobRecoverable"); - if (recoverable instanceof Boolean && (Boolean) recoverable) { - return true; - } - return false; + Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_KEY); + return Boolean.TRUE.equals(recoverable); }
|
|
||
| doc { | ||
|
|
||
| title "在这里输入结构的名称" |
There was a problem hiding this comment.
文档标题未正确填写
title "在这里输入结构的名称" 是占位符文本,应当替换为实际的结构名称,例如 "暂停长任务事件" 或 "APISuspendLongJobEvent"。
建议修复
- title "在这里输入结构的名称"
+ title "暂停长任务事件"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| title "在这里输入结构的名称" | |
| title "暂停长任务事件" |
🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy`
at line 8, Replace the placeholder title string in
APISuspendLongJobEventDoc_zh_cn.groovy: change the line title "在这里输入结构的名称" to a
proper Chinese title such as "暂停长任务事件" (or the English identifier
"APISuspendLongJobEvent") so the document header accurately reflects the
structure; update only the title(...) call in that file to the chosen
descriptive text.
| ref { | ||
| name "inventory" | ||
| path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory" | ||
| desc "null" | ||
| type "LongJobInventory" | ||
| since "5.5.0" | ||
| clz LongJobInventory.class | ||
| } | ||
| field { | ||
| name "success" | ||
| desc "" | ||
| type "boolean" | ||
| since "5.5.0" | ||
| } |
There was a problem hiding this comment.
字段描述缺失或为占位符
- Line 13:
desc "null"应描述 inventory 字段的含义 - Line 20:
desc ""应描述 success 字段的含义
建议修复
ref {
name "inventory"
path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory"
- desc "null"
+ desc "长任务详情"
type "LongJobInventory"
since "5.5.0"
clz LongJobInventory.class
}
field {
name "success"
- desc ""
+ desc "操作是否成功"
type "boolean"
since "5.5.0"
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ref { | |
| name "inventory" | |
| path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory" | |
| desc "null" | |
| type "LongJobInventory" | |
| since "5.5.0" | |
| clz LongJobInventory.class | |
| } | |
| field { | |
| name "success" | |
| desc "" | |
| type "boolean" | |
| since "5.5.0" | |
| } | |
| ref { | |
| name "inventory" | |
| path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory" | |
| desc "Long job inventory details" | |
| type "LongJobInventory" | |
| since "5.5.0" | |
| clz LongJobInventory.class | |
| } | |
| field { | |
| name "success" | |
| desc "Whether the operation succeeded" | |
| type "boolean" | |
| since "5.5.0" | |
| } |
🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy`
around lines 10 - 23, The documentation contains missing placeholder
descriptions for the APISuspendLongJobEvent fields; update the ref block for
"inventory" (path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory",
clz LongJobInventory) to include a concise Chinese description of what the
LongJobInventory represents (e.g., long job metadata being suspended), and
update the field block "success" to include a Chinese description indicating
whether the suspend operation succeeded (boolean true/false); ensure both desc
strings are meaningful, concise, and match existing doc style.
462a85c to
0ac6697
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java`:
- Around line 259-276: In sendCallback, the Content-Length is computed using
responseBody.length() (characters) which is incorrect for non-ASCII text; change
it to compute the byte length using a fixed charset (e.g.,
StandardCharsets.UTF_8) and pass that long to headers.setContentLength; also
handle null responseBody by using 0 bytes. Locate sendCallback and replace the
current headers.setContentLength(...) call with a computation like responseBody
== null ? 0 : responseBody.getBytes(StandardCharsets.UTF_8).length and ensure
the rest of the method (HttpEntity construction) continues to use the
responseBody string as before.
- Around line 227-237: The small-body branch in TestLibController currently uses
reader.readLine() which strips line terminators and corrupts multi-line request
bodies; change the implementation for the contentLength < 1MB branch to read raw
characters (e.g., via reader.read(char[] buffer) in a loop) into the existing
StringBuilder (sb), checking sb.length() against maxSize after each read, and
return sb.toString(); ensure this matches the large-file code path behavior
(lines handling reader.read into a buffer) and keep the same variables:
contentLength, maxSize, reader, and StringBuilder sb.
- Around line 281-297: The sendErrorCallback method builds JSON by string
formatting which can produce invalid JSON if error.getMessage() contains special
characters; fix it by replacing the manual String.format("{\"error\":\"%s\"}",
...) with proper JSON serialization (e.g., use the project ObjectMapper or
Jackson) to serialize a small map or POJO like Collections.singletonMap("error",
error.getMessage()) via objectMapper.writeValueAsString(...), then use that
serialized string as errorBody (keep HttpHeaders/HttpEntity usage unchanged) so
the error message is correctly escaped and valid JSON.
- Line 495: The getOutputStream() override currently throws
UnsupportedOperationException which breaks handlers that write via
getOutputStream(); update TestLibController.getOutputStream() to return a
ServletOutputStream backed by a ByteArrayOutputStream (shared with getWriter()
so both writers write to the same response buffer) and provide accessor methods
to retrieve the buffered bytes; if you prefer a minimal change, at least change
the exception message to explain why getOutputStream() is unsupported and
reference getWriter() as the alternative. Ensure the returned
ServletOutputStream implements write(int)/isReady()/setWriteListener(...) to
behave like a real stream and that getWriter() wraps the same
ByteArrayOutputStream to keep responses consistent.
🧹 Nitpick comments (2)
testlib/src/main/java/org/zstack/testlib/TestLibController.java (2)
327-327: 未使用的字段cachedReader。该字段已声明但从未被使用,可以移除。
♻️ 建议移除
private static class CachedHttpServletRequest implements HttpServletRequest { private final HttpServletRequest delegate; private final String cachedBody; - private BufferedReader cachedReader;
517-523: 字符编码硬编码为 UTF-8,应使用请求的编码。如果请求使用非 UTF-8 编码,这里的转换可能会导致内容损坏。此外,UTF-8 是 Java 保证支持的编码,
UnsupportedEncodingException永远不会抛出。♻️ 建议改进
- public CachedServletInputStream(String body) { - try { - this.bodyBytes = body != null ? body.getBytes("UTF-8") : new byte[0]; - } catch (java.io.UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + public CachedServletInputStream(String body) { + this.bodyBytes = body != null ? body.getBytes(java.nio.charset.StandardCharsets.UTF_8) : new byte[0]; }如需支持多种编码,可考虑将请求的
getCharacterEncoding()传入此类。
| if (contentLength > 0 && contentLength < 1024 * 1024) { | ||
| // For small bodies (< 1MB), use StringBuilder with known capacity | ||
| StringBuilder sb = new StringBuilder(contentLength); | ||
| String line; | ||
| while ((line = reader.readLine()) != null) { | ||
| sb.append(line); | ||
| if (sb.length() > maxSize) { | ||
| throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes"); | ||
| } | ||
| } | ||
| return sb.toString(); |
There was a problem hiding this comment.
readLine() 会丢失换行符,导致请求体内容损坏。
使用 reader.readLine() 读取内容时,换行符会被丢弃。对于包含换行符的 JSON 或多行文本内容,这会导致数据损坏。此外,这与 Line 239-251 的大文件处理路径行为不一致。
🐛 建议修复:统一使用字符缓冲区读取
- if (contentLength > 0 && contentLength < 1024 * 1024) {
- // For small bodies (< 1MB), use StringBuilder with known capacity
- StringBuilder sb = new StringBuilder(contentLength);
- String line;
- while ((line = reader.readLine()) != null) {
- sb.append(line);
- if (sb.length() > maxSize) {
- throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
- }
- }
- return sb.toString();
- } else {
- // For larger or unknown size, read in chunks with size tracking
- StringBuilder sb = new StringBuilder(8192);
- char[] buffer = new char[8192];
- int charsRead;
- long totalRead = 0;
- while ((charsRead = reader.read(buffer)) != -1) {
- totalRead += charsRead;
- if (totalRead > maxSize) {
- throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
- }
- sb.append(buffer, 0, charsRead);
- }
- return sb.toString();
+ // Use char buffer to preserve all characters including newlines
+ int initialCapacity = (contentLength > 0 && contentLength < 1024 * 1024) ? contentLength : 8192;
+ StringBuilder sb = new StringBuilder(initialCapacity);
+ char[] buffer = new char[8192];
+ int charsRead;
+ long totalRead = 0;
+ while ((charsRead = reader.read(buffer)) != -1) {
+ totalRead += charsRead;
+ if (totalRead > maxSize) {
+ throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes");
+ }
+ sb.append(buffer, 0, charsRead);
}
+ return sb.toString();🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` around lines
227 - 237, The small-body branch in TestLibController currently uses
reader.readLine() which strips line terminators and corrupts multi-line request
bodies; change the implementation for the contentLength < 1MB branch to read raw
characters (e.g., via reader.read(char[] buffer) in a loop) into the existing
StringBuilder (sb), checking sb.length() against maxSize after each read, and
return sb.toString(); ensure this matches the large-file code path behavior
(lines handling reader.read into a buffer) and keep the same variables:
contentLength, maxSize, reader, and StringBuilder sb.
| private void sendCallback(String callbackUrl, String taskUuid, String responseBody, int statusCode) { | ||
| try { | ||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.APPLICATION_JSON); | ||
| headers.set(RESTConstant.TASK_UUID, taskUuid); | ||
| headers.setContentLength(responseBody != null ? responseBody.length() : 0); | ||
|
|
||
| HttpEntity<String> entity = new HttpEntity<>(responseBody != null ? responseBody : "", headers); | ||
| ResponseEntity<String> response = callbackRestTemplate.exchange( | ||
| callbackUrl, HttpMethod.POST, entity, String.class); | ||
|
|
||
| logger.debug(String.format("Sent callback for taskUuid[%s] to %s, status: %s", | ||
| taskUuid, callbackUrl, response.getStatusCode())); | ||
| } catch (Exception e) { | ||
| logger.error(String.format("Failed to send callback for taskUuid[%s] to %s", | ||
| taskUuid, callbackUrl), e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Content-Length 使用字符数而非字节数,对非 ASCII 内容会不正确。
responseBody.length() 返回的是字符数,而 Content-Length 头应该是字节数。对于包含中文或其他多字节字符的响应体,这会导致长度不匹配。
🐛 建议修复
- headers.setContentLength(responseBody != null ? responseBody.length() : 0);
-
- HttpEntity<String> entity = new HttpEntity<>(responseBody != null ? responseBody : "", headers);
+ String body = responseBody != null ? responseBody : "";
+ byte[] bodyBytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+ headers.setContentLength(bodyBytes.length);
+
+ HttpEntity<String> entity = new HttpEntity<>(body, headers);🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` around lines
259 - 276, In sendCallback, the Content-Length is computed using
responseBody.length() (characters) which is incorrect for non-ASCII text; change
it to compute the byte length using a fixed charset (e.g.,
StandardCharsets.UTF_8) and pass that long to headers.setContentLength; also
handle null responseBody by using 0 bytes. Locate sendCallback and replace the
current headers.setContentLength(...) call with a computation like responseBody
== null ? 0 : responseBody.getBytes(StandardCharsets.UTF_8).length and ensure
the rest of the method (HttpEntity construction) continues to use the
responseBody string as before.
| private void sendErrorCallback(String callbackUrl, String taskUuid, Throwable error) { | ||
| try { | ||
| String errorBody = String.format("{\"error\":\"%s\"}", error.getMessage()); | ||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.APPLICATION_JSON); | ||
| headers.set(RESTConstant.TASK_UUID, taskUuid); | ||
| headers.setContentLength(errorBody.length()); | ||
|
|
||
| HttpEntity<String> entity = new HttpEntity<>(errorBody, headers); | ||
| callbackRestTemplate.exchange(callbackUrl, HttpMethod.POST, entity, String.class); | ||
|
|
||
| logger.debug(String.format("Sent error callback for taskUuid[%s] to %s", taskUuid, callbackUrl)); | ||
| } catch (Exception e) { | ||
| logger.error(String.format("Failed to send error callback for taskUuid[%s] to %s", | ||
| taskUuid, callbackUrl), e); | ||
| } | ||
| } |
There was a problem hiding this comment.
错误消息未进行 JSON 转义,可能生成无效的 JSON。
如果 error.getMessage() 包含引号、反斜杠或换行符等特殊字符,生成的 JSON 将是无效的。建议使用 JSON 序列化库来构建响应。
🐛 建议修复:使用 JSON 工具类进行转义
- String errorBody = String.format("{\"error\":\"%s\"}", error.getMessage());
+ String errorMsg = error.getMessage() != null ? error.getMessage() : "Unknown error";
+ // Escape special characters for JSON
+ errorMsg = errorMsg.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ String errorBody = String.format("{\"error\":\"%s\"}", errorMsg);或者更推荐使用项目中已有的 JSON 库(如 Jackson)进行序列化。
🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` around lines
281 - 297, The sendErrorCallback method builds JSON by string formatting which
can produce invalid JSON if error.getMessage() contains special characters; fix
it by replacing the manual String.format("{\"error\":\"%s\"}", ...) with proper
JSON serialization (e.g., use the project ObjectMapper or Jackson) to serialize
a small map or POJO like Collections.singletonMap("error", error.getMessage())
via objectMapper.writeValueAsString(...), then use that serialized string as
errorBody (keep HttpHeaders/HttpEntity usage unchanged) so the error message is
correctly escaped and valid JSON.
| @Override public void setContentLengthLong(long len) {} | ||
| @Override public String getCharacterEncoding() { return "UTF-8"; } | ||
| @Override public String getContentType() { return contentType; } | ||
| @Override public javax.servlet.ServletOutputStream getOutputStream() throws IOException { throw new UnsupportedOperationException("Use getWriter()"); } |
There was a problem hiding this comment.
getOutputStream() 抛出异常可能导致某些处理器无法正常工作。
如果被测试的 HTTP 处理器使用 getOutputStream() 而非 getWriter() 写入响应,这会导致测试意外失败。建议实现一个基于 ByteArrayOutputStream 的版本,或者至少在异常消息中说明原因。
💡 可选改进:实现 OutputStream 支持
+ private final java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();
+ private javax.servlet.ServletOutputStream servletOutputStream;
+
`@Override`
public javax.servlet.ServletOutputStream getOutputStream() throws IOException {
- throw new UnsupportedOperationException("Use getWriter()");
+ if (servletOutputStream == null) {
+ servletOutputStream = new javax.servlet.ServletOutputStream() {
+ `@Override`
+ public void write(int b) throws IOException {
+ outputStream.write(b);
+ }
+ `@Override`
+ public boolean isReady() { return true; }
+ `@Override`
+ public void setWriteListener(javax.servlet.WriteListener listener) {}
+ };
+ }
+ return servletOutputStream;
}
+
+ public String getContentAsString() {
+ printWriter.flush();
+ String writerContent = writer.toString();
+ if (!writerContent.isEmpty()) {
+ return writerContent;
+ }
+ return outputStream.toString(java.nio.charset.StandardCharsets.UTF_8);
+ }🤖 Prompt for AI Agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java` at line 495,
The getOutputStream() override currently throws UnsupportedOperationException
which breaks handlers that write via getOutputStream(); update
TestLibController.getOutputStream() to return a ServletOutputStream backed by a
ByteArrayOutputStream (shared with getWriter() so both writers write to the same
response buffer) and provide accessor methods to retrieve the buffered bytes; if
you prefer a minimal change, at least change the exception message to explain
why getOutputStream() is unsupported and reference getWriter() as the
alternative. Ensure the returned ServletOutputStream implements
write(int)/isReady()/setWriteListener(...) to behave like a real stream and that
getWriter() wraps the same ByteArrayOutputStream to keep responses consistent.
0ac6697 to
a0bd0d6
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java`:
- Around line 83-106: 在 handleFullyAsync 中不要让后台线程通过 CachedHttpServletRequest 的
delegate 访问原始 HttpServletRequest;在发送 response 之前用 readRequestBodySafely
读取正文并快照所有后台处理需要的数据(例如 URI、头部 headers、查询参数/表单参数、方法等),然后构造一个独立的、纯数据的请求 DTO
或一个不再委托原始 request 的 CachedHttpServletRequest 替代品,并把该独立对象传给
Test.handleHttp(…)(MockHttpServletResponse 保留用于捕获输出);确保后台任务中的任何
getHeader()/getRequestURI()/getParameter() 调用都来自快照对象而非原始
delegate,以避免在响应提交后访问被回收/复用的 request。
♻️ Duplicate comments (4)
testlib/src/main/java/org/zstack/testlib/TestLibController.java (4)
224-236:readLine()会丢失换行符,导致请求体损坏
小体积分支用readLine()会丢弃换行符,多行 JSON/文本会被破坏,且与大体积分支行为不一致。✅ 建议修复(统一用字符缓冲区读取)
- if (contentLength > 0 && contentLength < 1024 * 1024) { - // For small bodies (< 1MB), use StringBuilder with known capacity - StringBuilder sb = new StringBuilder(contentLength); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - if (sb.length() > maxSize) { - throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes"); - } - } - return sb.toString(); - } else { - // For larger or unknown size, read in chunks with size tracking - StringBuilder sb = new StringBuilder(8192); - char[] buffer = new char[8192]; - int charsRead; - long totalRead = 0; - while ((charsRead = reader.read(buffer)) != -1) { - totalRead += charsRead; - if (totalRead > maxSize) { - throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes"); - } - sb.append(buffer, 0, charsRead); - } - return sb.toString(); - } + // Use char buffer to preserve all characters including newlines + int initialCapacity = (contentLength > 0 && contentLength < 1024 * 1024) ? contentLength : 8192; + StringBuilder sb = new StringBuilder(initialCapacity); + char[] buffer = new char[8192]; + int charsRead; + long totalRead = 0; + while ((charsRead = reader.read(buffer)) != -1) { + totalRead += charsRead; + if (totalRead > maxSize) { + throw new IOException("Request body exceeds maximum size: " + maxSize + " bytes"); + } + sb.append(buffer, 0, charsRead); + } + return sb.toString();
258-266:Content-Length使用字符数会导致长度不准确
responseBody.length()是字符数,非字节数;包含非 ASCII 时会错误。✅ 建议修复(按 UTF‑8 字节长度)
- headers.setContentLength(responseBody != null ? responseBody.length() : 0); - - HttpEntity<String> entity = new HttpEntity<>(responseBody != null ? responseBody : "", headers); + String body = responseBody != null ? responseBody : ""; + byte[] bodyBytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8); + headers.setContentLength(bodyBytes.length); + + HttpEntity<String> entity = new HttpEntity<>(body, headers);
280-288: 错误 JSON 需要转义,避免生成无效 JSON
String.format直接拼接error.getMessage()时,遇到引号/反斜杠/换行会破坏 JSON。建议至少做转义(或改用 JSON 序列化工具)。✅ 建议修复(最小转义 + 正确 Content-Length)
- String errorBody = String.format("{\"error\":\"%s\"}", error.getMessage()); + String errorMsg = error.getMessage() != null ? error.getMessage() : "Unknown error"; + errorMsg = errorMsg.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + String errorBody = String.format("{\"error\":\"%s\"}", errorMsg); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(RESTConstant.TASK_UUID, taskUuid); - headers.setContentLength(errorBody.length()); + byte[] bodyBytes = errorBody.getBytes(java.nio.charset.StandardCharsets.UTF_8); + headers.setContentLength(bodyBytes.length);
499-499:getOutputStream()直接抛异常会使部分处理器测试失败
若被测处理器使用getOutputStream()写响应,将导致测试异常。建议提供基于ByteArrayOutputStream的实现或明确兼容方式。
testlib/src/main/java/org/zstack/testlib/TestLibController.java
Outdated
Show resolved
Hide resolved
bc0977b to
3dcf84d
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@testlib/src/main/java/org/zstack/testlib/TestLibController.java`:
- Around line 118-120: Replace the Chinese inline comments in
TestLibController.java with equivalent English comments that convey the same
intent: describe that after the simulator finishes processing an active HTTP
callback is sent, note that EnvSpec.replyHttpCall may have already sent a
callback but we proactively send one here to ensure reliability in case
EnvSpec.replyHttpCall did not execute. Update the comments near the Simulator
handling and the EnvSpec.replyHttpCall mention so they are fully in English
while preserving original meaning and context.
♻️ Duplicate comments (5)
testlib/src/main/java/org/zstack/testlib/TestLibController.java (5)
247-257: 小体积分支仍会丢失换行符
reader.readLine()会丢弃换行符,导致多行 JSON/文本被破坏,且与大体积分支行为不一致。建议统一使用字符缓冲读取。
281-286: Content-Length 应使用字节长度
responseBody.length()是字符数,非字节数。对非 ASCII 内容会导致 Content-Length 不准确。
301-309: 错误回调 JSON 未做转义
String.format("{\"error\":\"%s\"}", error.getMessage())未转义特殊字符,可能生成无效 JSON。建议使用 JSON 序列化。
378-403: 异步线程仍依赖原始 HttpServletRequest后台线程通过
delegate访问请求对象(如getHeader()/getRequestURI()),而响应已提交后容器不保证请求对象仍可用,可能导致不稳定。建议在返回前快照所需字段并改用独立对象传递。
531-531: MockHttpServletResponse 不支持 getOutputStream若被测处理器使用
getOutputStream(),这里会抛异常导致测试失败。建议提供基于缓冲的实现或更明确的降级策略。
testlib/src/main/java/org/zstack/testlib/TestLibController.java
Outdated
Show resolved
Hide resolved
e2df4a9 to
35d4231
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@header/src/main/java/org/zstack/header/longjob/LongJob.java`:
- Around line 13-14: Add Javadoc to the new LongJob.suspend(LongJobVO,
ReturnValueCompletion<Boolean>) method (and update resume(LongJobVO,
ReturnValueCompletion<APIEvent>) if missing) that states this default
implementation is a no-op, documents the semantics of the parameters (LongJobVO
job and the completion callback), and clearly describes the expected behavior
for calling completion (e.g., invoke completion.success(true/false) on success
or completion.fail(Throwable) on error). Reference the LongJob interface and the
exact method signatures so the doc clarifies default behavior and the
caller/implementer contract for invoking the provided ReturnValueCompletion.
In `@longjob/src/main/java/org/zstack/longjob/LongJobUtils.java`:
- Around line 165-190: Extract the magic string "longJobRecoverable" into a
static final constant (e.g., LONG_JOB_RECOVERABLE) in LongJobUtils and replace
its usages inside isRecoverableError(ErrorCode) with that constant; also harden
isRecoverableError by null-checking errorCode and the opaque value (ensure
errorCode != null, getFromOpaque(...) != null) before instanceof/boolean checks
and return false on nulls to avoid NPEs.
♻️ Duplicate comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java (1)
487-493: 必须处理 suspended 返回值,否则会错误标记为 Suspended。
Line 489-492 直接切换到 Suspended,忽略了suspended的实际结果,可能导致状态与真实暂停结果不一致。🔧 建议修正
job.suspend(vo, new ReturnValueCompletion<Boolean>(completion) { `@Override` public void success(Boolean suspended) { - LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); - logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); - completion.success(updatedVo); + if (Boolean.TRUE.equals(suspended)) { + LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); + logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); + completion.success(updatedVo); + } else { + logger.info(String.format("longjob [uuid:%s, name:%s] suspend returned false; keep state unchanged", + vo.getUuid(), vo.getName())); + completion.success(dbf.findByUuid(uuid, LongJobVO.class)); + } }
| default void suspend(LongJobVO job, ReturnValueCompletion<Boolean> completion) {} | ||
| default void resume(LongJobVO job, ReturnValueCompletion<APIEvent> completion) {} |
There was a problem hiding this comment.
新增接口方法请补充 Javadoc 说明。
suspend(...) 是新接口方法,按规范需 Javadoc,建议说明默认 no-op 行为及 completion 的期望调用方式。根据编码规范。
📝 建议补充的注释示例
+ /**
+ * Suspend the long job if supported.
+ * Default implementation is no-op; implementations should invoke completion.
+ */
default void suspend(LongJobVO job, ReturnValueCompletion<Boolean> completion) {}🤖 Prompt for AI Agents
In `@header/src/main/java/org/zstack/header/longjob/LongJob.java` around lines 13
- 14, Add Javadoc to the new LongJob.suspend(LongJobVO,
ReturnValueCompletion<Boolean>) method (and update resume(LongJobVO,
ReturnValueCompletion<APIEvent>) if missing) that states this default
implementation is a no-op, documents the semantics of the parameters (LongJobVO
job and the completion callback), and clearly describes the expected behavior
for calling completion (e.g., invoke completion.success(true/false) on success
or completion.fail(Throwable) on error). Reference the LongJob interface and the
exact method signatures so the doc clarifies default behavior and the
caller/implementer contract for invoking the provided ReturnValueCompletion.
| } else if (isRecoverableError(errorCode)) { | ||
| return LongJobStateEvent.suspend; | ||
| } else { | ||
| return LongJobStateEvent.fail; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if an error is marked as recoverable in its opaque field. | ||
| * Any business module can mark an error as recoverable by setting | ||
| * "longJobRecoverable" to true in the error code's opaque field. | ||
| * This allows the long job framework to automatically suspend instead of fail | ||
| * for recoverable errors, enabling automatic retry after service restart. | ||
| * | ||
| * @param errorCode the error code to check | ||
| * @return true if the error is marked as recoverable, false otherwise | ||
| */ | ||
| private static boolean isRecoverableError(ErrorCode errorCode) { | ||
| // Check if error code has recoverable flag in opaque | ||
| // Any business module can set this flag to indicate the error is recoverable | ||
| Object recoverable = errorCode.getFromOpaque("longJobRecoverable"); | ||
| if (recoverable instanceof Boolean && (Boolean) recoverable) { | ||
| return true; | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. 查找 getEventOnError 方法定义和调用点
echo "=== Looking for getEventOnError definition and calls ==="
rg -nP --type=java 'getEventOnError' longjob/
# 2. 查找相关的 opaque key 常量是否已存在
echo -e "\n=== Checking for existing OPAQUE constants ==="
rg -nP --type=java 'longJobRecoverable|OPAQUE.*KEY' longjob/
# 3. 查找 ErrorCode 处理模式
echo -e "\n=== Checking ErrorCode null handling patterns ==="
rg -nP --type=java 'if.*errorCode\s*==\s*null|errorCode\.get' longjob/ | head -20Repository: MatheMatrix/zstack
Length of output: 920
🏁 Script executed:
#!/bin/bash
# 查看 LongJobManagerImpl.java 第 721 行的上下文,了解 errorCode 是否可能为空
echo "=== Context around line 721 in LongJobManagerImpl.java ==="
sed -n '710,735p' longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java | cat -n
# 也需要查看完整的 getEventOnError 方法定义
echo -e "\n=== Full getEventOnError method ==="
sed -n '160,192p' longjob/src/main/java/org/zstack/longjob/LongJobUtils.java | cat -nRepository: MatheMatrix/zstack
Length of output: 3296
🏁 Script executed:
#!/bin/bash
# 查看 fail 回调方法是否来自接口定义,以及 ErrorCode 是否应该允许为空
echo "=== Search for fail callback interface ==="
rg -nP --type=java 'void\s+fail\s*\(\s*ErrorCode' longjob/ | head -5
# 查看 ZStack 中 ErrorCode 的设计模式
echo -e "\n=== Check ErrorCode null patterns in broader codebase ==="
fd --type f -name "*.java" header | xargs rg -l 'ErrorCode.*null|null.*ErrorCode' | head -5
# 检查这个 opaque key 是否在其他地方被使用或定义
echo -e "\n=== Search for similar opaque key patterns ==="
rg -nP --type=java 'getFromOpaque|setToOpaque' longjob/ | head -10Repository: MatheMatrix/zstack
Length of output: 1646
🏁 Script executed:
#!/bin/bash
# 查看 ErrorCode 接口/类的定义,了解它是否应该允许为空
echo "=== Check ErrorCode definition ==="
fd --type f -name "ErrorCode.java" | xargs cat -n | head -50
# 查看更多 getFromOpaque 的使用模式,了解是否有其他地方定义了相关常量
echo -e "\n=== Look for opaque key constants in ErrorCode or related classes ==="
rg -nP --type=java 'OPAQUE|Opaque' header/ | grep -i key | head -10
# 检查是否有已有的常量定义模式
echo -e "\n=== Check for constant definitions for opaque keys ==="
rg -nP --type=java 'static\s+final\s+String.*=' header/ | grep -i opaque | head -10Repository: MatheMatrix/zstack
Length of output: 933
🏁 Script executed:
#!/bin/bash
# 查看 ErrorCode.java 的完整定义
echo "=== Check ErrorCode definition ==="
find . -name "ErrorCode.java" -path "*/header/*" -exec cat {} \; | head -100
# 看看 opaque 相关的实现
echo -e "\n=== Check opaque implementation in ErrorCode ==="
find . -name "ErrorCode.java" -path "*/header/*" -exec grep -A5 "putToOpaque\|getFromOpaque" {} \;Repository: MatheMatrix/zstack
Length of output: 3078
将 opaque key 字符串提取为常量
根据编码规范,应避免魔法值。建议将 "longJobRecoverable" 字符串定义为 static final 常量,便于跨模块复用并防止拼写错误。此外,建议在 isRecoverableError() 方法中增加防御性的 null 检查,提高代码鲁棒性。
建议修改
+ private static final String LONG_JOB_RECOVERABLE_OPAQUE_KEY = "longJobRecoverable";
+
private static boolean isRecoverableError(ErrorCode errorCode) {
+ if (errorCode == null) {
+ return false;
+ }
- Object recoverable = errorCode.getFromOpaque("longJobRecoverable");
- if (recoverable instanceof Boolean && (Boolean) recoverable) {
- return true;
- }
- return false;
+ Object recoverable = errorCode.getFromOpaque(LONG_JOB_RECOVERABLE_OPAQUE_KEY);
+ return Boolean.TRUE.equals(recoverable);
}🤖 Prompt for AI Agents
In `@longjob/src/main/java/org/zstack/longjob/LongJobUtils.java` around lines 165
- 190, Extract the magic string "longJobRecoverable" into a static final
constant (e.g., LONG_JOB_RECOVERABLE) in LongJobUtils and replace its usages
inside isRecoverableError(ErrorCode) with that constant; also harden
isRecoverableError by null-checking errorCode and the opaque value (ensure
errorCode != null, getFromOpaque(...) != null) before instanceof/boolean checks
and return false on nulls to avoid NPEs.
35d4231 to
0fe93a9
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy`:
- Line 20: 在 APISuspendLongJobMsgDoc_zh_cn.groovy 中,当前的请求描述字段 desc """""" 为空;请在类
APISuspendLongJobMsgDoc_zh_cn 或与其对应的描述块中为 desc 添加简短明确的中文说明(例如说明此 API
用于挂起指定的长任务并返回操作结果),以便文档完整并能清晰表达请求用途;定位时查找文件中名为 APISuspendLongJobMsgDoc_zh_cn
的文档定义并更新其 desc 字符串。
- Line 10: Replace the placeholder API description string in
APISuspendLongJobMsgDoc_zh_cn.groovy (the desc """在这里填写API描述""") with a concise,
accurate Chinese description of the API’s behavior (for example:
"""暂停一个长时间运行的任务""" or another phrasing that accurately describes that the API
suspends a long-running job), ensuring the desc field clearly explains the
endpoint's purpose and expected effect.
🧹 Nitpick comments (1)
longjob/src/main/java/org/zstack/longjob/LongJobFactory.java (1)
10-15: 为新增接口方法补充 Javadoc 说明
Line 14 新增的supportSuspend建议补充英文 Javadoc,明确语义与返回值,便于 API 文档与维护一致。As per coding guidelines, please add Javadoc for interface methods.♻️ 建议修改
+ /** + * Whether the long job supports suspend operation. + */ boolean supportSuspend(String jobName);
|
|
||
| category "longjob" | ||
|
|
||
| desc """在这里填写API描述""" |
There was a problem hiding this comment.
API 描述为占位符,需要填写实际内容。
当前描述 """在这里填写API描述""" 是占位符文本。建议替换为实际的 API 功能描述,例如:"""暂停一个长时间运行的任务"""。
🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy`
at line 10, Replace the placeholder API description string in
APISuspendLongJobMsgDoc_zh_cn.groovy (the desc """在这里填写API描述""") with a concise,
accurate Chinese description of the API’s behavior (for example:
"""暂停一个长时间运行的任务""" or another phrasing that accurately describes that the API
suspends a long-running job), ensuring the desc field clearly explains the
endpoint's purpose and expected effect.
|
|
||
| clz APISuspendLongJobMsg.class | ||
|
|
||
| desc """""" |
There was a problem hiding this comment.
请求描述为空。
desc """""" 为空,建议添加对请求的简要描述。
🤖 Prompt for AI Agents
In
`@header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy`
at line 20, 在 APISuspendLongJobMsgDoc_zh_cn.groovy 中,当前的请求描述字段 desc """"""
为空;请在类 APISuspendLongJobMsgDoc_zh_cn 或与其对应的描述块中为 desc 添加简短明确的中文说明(例如说明此 API
用于挂起指定的长任务并返回操作结果),以便文档完整并能清晰表达请求用途;定位时查找文件中名为 APISuspendLongJobMsgDoc_zh_cn
的文档定义并更新其 desc 字符串。
APIImpact Resolves: ZSTAC-81237 Change-Id: I6f7262716c7075637373646b726a796c73737563 Signed-off-by: AlanJager <ye.zou@zstack.io>
0fe93a9 to
7dac565
Compare
APIImpact
Resolves: ZSTAC-81237
Change-Id: I6f7262716c7075637373646b726a796c73737563
sync from gitlab !9059